在上一篇文章有提到,VS2012將原本測試非public method的功能移除了。
而今天也剛好有朋友問到,為什麼這麼好用的功能已經作出來了,還特地要移除呢?測試非public的方法,會有什麼問題嗎?
我連非public的方法都測了,品質應該更好吧?
這一篇文章,將從物件導向的觀點,以及維護的成本進行一些說明。也希望可以替心裡有這些疑惑的朋友們,帶來不一樣角度的思考。
上一篇文章:[Day 3]動手寫Unit Test
本系列文章專區
@前言
在 Visual Studio 2012 中,針對 Unit Test 的部份,有一個重要的變動:
原本針對「測試物件非 public 的部分」,可透過 Visual Studio 2010 自動產生的 accessor 來進行測試。在 Visual Studio 2012 中,將此功能移除了。
Accessor 其背後的原理,是將物件透過很「髒」的 reflection 方式,把物件內所有的東西 public 出來。並且 Visual Studio 在異動物件後,進行與設計測試時,會幫你做同步產生 accessor 的動作。(實際的原理我沒有深入研究,也不太確定。但基本上的概念就是如此)
這個原本被認為很方便、實用的功能(包括我很久之前寫測試時,也是這麼認為),很抱歉,在 Visual Studio 2012 中移除了。接下來說明,單元測試是否應該對測試物件非 public 的部份,進行單元測試。
@單元測試的意義
一言以蔽之:「單元測試就是用來模擬外部如何使用測試目標物件,驗證其行為是否符合預期」。
因此,有個重點是:外部如何使用測試目標物件。
讓我們回到 Object-Oriented 的封裝原則,封裝的用意在於:
有了對單元測試與封裝的認知後,接下來說明,為什麼單元測試只需要針對測試目標物件 public 的行為,進行測試即可。為什麼 Visual Studio 2012 要把 accessor 的功能移除。(不過這純屬我自己從單元測試意義當出發點的推論)
@只測試 Public 行為?
根據單元測試的意義,以及封裝的用意,代表著「外部使用者原本就不需要了解,也根本不了解,測試目標物件非public的行為」。單元測試既然是模擬外部使用端的動作,那當然只針對測試目標物件 public 的行為進行模擬與驗證。
但一些朋友肯定有些疑惑,那非 public 的 method 該怎麼辦?不測嗎?那 code coverage 怎麼提昇?要怎麼知道這些非 public 的行為有沒如同預期般運作呢?
有這些疑問是正常的,因為我一開始也是有一模一樣的疑問,但開始接觸 TDD 之後,反而更加了解了 Unit Test 的本質。
所謂的非 public 的行為,其存在的原因,一定是因為某一些 public 的行為會用到這些 private 或 protected 的 method,如果物件中存在著跟 public method 無關的 private 或 protected method,那在設計上就是個問題,這些非 public 的 method 根本就沒有存在的意義。因為外部使用測試目標物件時,完全不會用到這些 method,就像宣告了變數卻不去使用它一樣,沒有意義。
而當 private 或 protected method 與 public method有關時,那針對 public method 的 Unit Test 便會涵蓋到這些 private 或 protected method,它們就是 public method 的一部分,對外部使用者來說,根本分辨不出來什麼是 private 或 protected,因為只關注在物件外部可視行為上。
所以,在實作單元測試上,倘若測試物件一個 public method 中,涵蓋了一個 private method,而 private method 中與外部物件或服務相依,那麼在測這個 public method 時,要連 private method 中相依的 interface ,都要撰寫 stub object 來模擬才行,這也是為什麼單元測試被稱為白箱測試的原因。但還是得強調一次,外部使用者是無法分清楚哪一部分是 public method 內容,哪一部分是非 public method。
總結上面的說法,非 public method 的測試涵蓋率,是依據 public method 呼叫時的 input 來決定。
有沒有可能,當 public method 該測的都測了,甚至 public method 主體內容涵蓋率都 100% 了,非 public 的部分涵蓋率卻很低?當然有可能,但這要釐清一下,沒有被涵蓋到的部份,是屬於什麼樣的程式碼。
如果在非 public method 中,沒被測試覆蓋的部份,是防呆、斷言之類的程式碼,那麼是屬於正常的情況。因為可能在呼叫非 public method 之前,就已經先防呆了,導致非 public method 中的防呆永遠不會發生。但,因為系統的健壯性考量,該斷言、防呆、驗證的部份,還是不能少。因為不會知道未來其他方法呼叫前,有沒做好防呆的部份。
那麼,在 private 或 protected method 中,非防呆、斷言的程式碼,卻又沒被涵蓋到部分呢?這是個警訊,代表著這些程式碼可能是 over design,或是根本沒有用處。因為這個物件所有對外的行為,所有的可能性,都模擬過一次了,卻都不會用到這些沒被涵蓋到的程式碼,這不就代表「這些程式碼目前用不到」嗎?YAGNI 原則就是在說這件事:「You ain't gonna need it !」
只要 public 的行為如同預期,即使 private 或 protected 的 method 是 hard-code,是很沒彈性,是很愚蠢的寫法,對外部使用來說,根本就不在乎,因為無感。
這也是 TDD 所提倡的精神,如果所有使用行為都符合預期,就代表功能完成了。而且依據測試來撰寫的 production code,幾乎不會出現測試涵蓋不到的 code,因為 production code 是為了滿足測試而撰寫的。不需要存在用不到的 production code,因此,也可以避免 over design 的情況。
@針對非 public 行為測試又如何?
上面那一段的說明,肯定還是無法說服所有人,「為什麼要把已經存在的功能移除?」
不用 accessor 的人大可不用,但已經在用,或真的得用的人,還是希望可以在 VS2012 中繼續使用。
回到封裝的用意上,「封裝變化」一直是物件導向設計中很重要的設計原則。那些針對 private 與 protected 進行單元測試的朋友,有沒有過「因為一些需求異動,導致單元測試程式就需要跟著重新調整、設計或修改,而且頻率與範圍導致測試的維護成本增加不少」的經驗。如果有,這就是為什麼不希望 developer 去針對非 public method 寫單元測試的原因。
著重在非 public method 的單元測試,說穿了只是寫給 developer 爽而已。因為要封裝變化,才會把這些內容變成 private 或 protected,以期望變化時對外部使用者來說,呈現無感,也就是降低耦合,也就是最小知識原則。
現在單元測試卻透過某些機制,來存取這些封裝起來的行為,不是自討苦吃嗎?原本就知道,這些東西很可能會一直變化,卻又去存取它,測試它,導致單元測試因此維護與異動頻率增加,這不就違背了封裝的用意?
★★★
對使用物件的角度來說,使用端根本不關心這些變化,卻因為單元測試用髒方法硬幹到這些不公開的行為,導致測試成本增加,進而導致一些不明就裡的 developer 喊出「測試很花成本,時間增加很多,很難維護」。我只想說:「這不是南北拳的問題,是你的問題。」
★★★
@結論
說真的,剛知道 Visual Studio 2012 把 accessor 功能拿掉,我也一整個相當吃驚,覺得要強迫 developer 用 TDD 方式開發,也不用做到這麼絕吧。
但將物件導向的原則、TDD 的精神、單元測試的基本意義結合起來後,有了上述的思考歷程,就覺得只測試 public method,不建議測試 private 與 protected method,是一件正確且重要的事。
所以將這樣的思考與推論過程,分享給各位朋友參考,不一定完全符合 Visual Studio 2012 移除 accessor 的原因,這只是我自己的理解與想法而已,但從我一開始接觸單元測試,怎麼測 private method 就一直困擾我很久,雖說腦袋中有點輪廓,卻一直無法明確釐清。
@補充
這邊有一篇寫的很不錯的文章,講的相當全面,包括概念、現實上的考量、過程中的考量,都寫得很清楚。請參考:Testing Private Methods with JUnit and SuiteRunner
@讀者的疑問
針對讀者的一些疑問,我就補充在文末,大家若還有什麼想了解或發問的,歡迎留言。
Q1. 文章上只提到了public, protected, private,那麼internal呢?
答: 這是一個很棒的問題,因為我文中的確沒提到internal的部份。
首先internal的定義/用意,是指在同一組件內才能看的到,也就是我這物件希望在我這組件裡是公開的,但組件外的人看不到也用不到。(這樣設計可以有效控制相依範圍)
而單元測試如前面所說,是針對「物件」的互動,來進行模擬使用。那宣告成internal的物件,到底要不要測試,當然要,因為的確有其他物件會使用它,我們就要思考:「怎麼使用它」。
但一般測試專案的角度來看,是參考production code的Library,所以對測試專案的角度,是看不到production code裡面宣告成internal的物件的,但我又想去測試production code中internal的物件,該怎麼辦?
在.NET中相當簡單,只需要透過:InternalsVisibleToAttribute 這個屬性設定即可。將production code library指定給Test project可見,就可以解決這個問題。
Q2. 若沒針對private測試,當發生問題時,我怎麼知道是哪一段code錯了?或是它沒被涵蓋到,就代表沒有受到測試保護。
答:這個問題,就是慢慢消化這篇文章,並實際動手做之後,就會漸漸的撥開雲霧見青天了。
當只用測試的思維來看,那不去「針對」private method測試,是一件很奇怪的事,因為它活著,但沒有測試可以馬上知道它對不對。
這也是跨入TDD的其中一道門檻。回過頭來看前幾篇的宗旨,系統的存在,到底為了什麼?
為了可以正確的滿足使用者的需求,外部使用的需求。既然用了物件導向來設計,既然把這些東西封裝起來,外部的使用者就根本看不到、用不到,也不該看到用到。而我們封裝的意義就在於封裝變化。這時候用其他方式,硬幹進去物件中去測試private method,也只是增加自己未來的負擔,因為它肯定會一直變。
原本private的改變,可以幾乎不影響任何部分,除了物件本身內部。所以它可以放變化。
現在外面看的到這個方法,你就不能輕易改變,一旦要改,可能會影響許多測試程式,反倒是production code不會有太多影響。但測試程式如果因此要維護或是要重寫,這就都是根本沒必要的東西。
最後,如果你用TDD的方式開發,就根本更不會碰到這個問題。
因為,你只針對public行為,來進行預期,永遠切入點都是撰寫public的內容。大概只有重構的時候,會出現private跟protected。而這個時候,被放到private的方法,當然是你原本放在public方法內的內容。
那如果原本public方法code coverage是100%,那也不會因為你搬到private,code coverage就變成50%。如果出現了因為重構,就沒有涵蓋到的範圍,那就是over design的bad smell,是個徵兆。
這邊就是需要搭配TDD與Refactoring的手法,才能一體成型,享受其美妙之處而無後顧之憂。
再強調一次,private/protected的方法內容,在TDD裡面,基本上都是因為refactoring的extract method所產生的,都是一些原本放在public/internal的function內容。而不會是直接動手去寫private function,除非你是top-down的先訂出程式的骨頭。但最終,private function仍屬於public function內容的一部分。
所以要特別滿足的應該是:您是否有針對外部可見的行為,進行了所有具代表性的情境來做測試。如果真的涵蓋了所有,包括exception handling,那麼這個物件內,沒被涵蓋到的部份,基本上都可以刪除了。絕不會對外部使用造成任何影響。
針對 private method 該不該測這個問題,我一開始是覺得一定要測!不然,我的 public method 如果有問題,我怎麼知道是那個 private method 引起的呢?可是到後來我又覺得,測那麼多 private method 到最後只會讓我的 test case 很髒~要 refactor 的時候很綁手綁腳…
最後我突然有種感覺, private method 還是可以測,可是僅次於開發的當下。當開發完成後, private method 的 test case 還是拿掉比較好。就把 private method 當作像是蓋房子的鷹架一樣,當作輔助就好。因為,只單純測試 public method 的話,code coverage 一樣是會涵蓋到 private method (public method 會去用 private method),如果無論如何都測不到 private method (那就很有可能代表著此 private method 用不到)那又何必測呢!
應該是這樣吧!這是我個人的小小感想~
以思路歷程來說,差不多就是像你說的這樣...最後就是覺得何必測。
我想拉出來highlight的一點是,如果你只用測試的思維來看,那不去「針對」private method測試,是一件很奇怪的事,因為它活著,但沒有測試可以馬上知道它對不對。
這也是跨入TDD的其中一道門檻。回過頭來看前幾篇的宗旨,系統的存在,到底為了什麼?
為了可以正確的滿足使用者的需求,外部使用的需求。既然用了物件導向來設計,既然把這些東西封裝起來,外部的使用者就根本看不到、用不到,也不該看到用到。而我們封裝的意義就在於封裝變化。這時候用其他方式,硬幹進去物件中去測試private method,也只是增加自己未來的負擔,因為它肯定會一直變。
原本private的改變,可以幾乎不影響任何部分,除了物件本身內部。所以它可以放變化。
現在外面看的到這個方法,你就不能輕易改變,一旦要改,可能會影響許多測試程式,反倒是production code不會有太多影響。但測試程式如果因此要維護或是要重寫,這就都是根本沒必要的東西。
最後,如果你用TDD的方式開發,就根本更不會碰到這個問題。
因為,你只針對public行為,來進行預期,永遠切入點都是撰寫public的內容。大概只有重構的時候,會出現private跟protected。而這個時候,被放到private的方法,當然是你原本放在public方法內的內容。
那如果原本public方法code coverage是100%,那也不會因為你搬到private,code coverage就變成50%。如果出現了因為重構,就沒有涵蓋到的範圍,那就是over design的bad smell,是個徵兆。
不曉得這樣補充說明,有沒有讓您更打通了任督二脈。
聽君一席話勝讀十年書阿!
不但打通了任督二脈,連我是誰都忘了
所以一開始根本不會有 private 或 protected 的存在對吧!無論是 private method 或 protected method, 都是在寫完整個測試及 production code 之後才會出現的產物!
所以如果要TDD,重點就是一開始就要先想好,我到底需要哪些 public method. 這個就是進入TDD的門檻~對吧~